package game;


import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;


/**
 * Steuert das Spiel und verbindet alle Spielobjekte
 * 
 * @author Lars
 */
public class Arena implements Drawable {
    
    
    static int ARENAWIDTH;
    static int ARENAHEIGHT;
    
    //Attribute
    private Rectangle area;
    private int amountPlayers;
    private Pong pong;
    private Racket[] rackets; //Spielerbalken; Racket 0: links, Racket 1: rechts, Racket 2: unten, Racket 3: oben
    private Border[] borders; //Fixe Spielfeldbegrenzungen
    private Playerlifes[] players; //Punktezähler am oberen Bildschirmrand
    private float startSpeedPong; //Startgeschwindigkeit des Pongs, bei jedem Aufruf von resetPong() benutzt
    
    //Physikalische Einstellungen
    private final float INITIALSPEEDPONG = 2.5f; //Initialgeschiwindigkeit des Pongs in Promille zur Arenabreite. Verwendet beim Starten des Spiels
    private final float RACKETSPEED = 0.0025f; //Geschwindigkeit der Rackets
    private final float DERIVATION = (float) (Math.PI/2); //Ablenkungswinkel beim Treffen der Rackets am unteren und oberen Ende (fließende Skala)
                                                          //Angabe des doppelten, maximalen Anblenkungswinkels in Teilen von 2*Pi (Rad-Skala)
    private final float ACCELLERATION = 1.05f; //Beschleunigungsfaktor nach einer Racketkollission
    private final float INITACCELLERATION = 1.1f; //Beschleunigungsfaktor nach jedem Zurücksetzen des Balls
    
    //Groesseneinstellungen
    private final int PONGDIAMETER = 30; //Durchmesser des Pongs in Promille zur Arena Breite
    private final int RACKETLENGTH = 20; //Racket Laenge in Prozent zur Arena Höhe
    
    //Bugfix Logs
    private final boolean BUGFIX = false;
    
    //Methoden

    /**
     * Konstruktor
     * @param width minimal: ca. 1000
     * @param height minimal: ca. 1000
     */
    Arena(int width, int height){
        Arena.ARENAHEIGHT = width;
        Arena.ARENAWIDTH = height;
        
        this.area = new Rectangle(0,0,Arena.ARENAWIDTH,Arena.ARENAHEIGHT);
        this.pong = new Pong();
        this.startSpeedPong = INITIALSPEEDPONG;
    }
    
    /**
     * Startet 3-Spieler-Spiel.
     * Initialisiert alle Objekte und setzt Pong zurück.
     * @return Zeiger auf alle zu rendernden Objekte.
     */
    public Drawable[] initThreePlayerGame(){
        this.amountPlayers = 3;
        
   
        //-----------------------------
        this.borders = new Border[3];
        this.borders[0] = new Border(0,0,this.area.getWidth(),this.area.getHeight()/20);
        
        int length = this.area.getHeight()/20;
        int thickness = this.area.getHeight()/20;
        this.borders[1] = new Border(0,this.area.getHeight()-thickness, length, thickness);
        this.borders[2] = new Border(this.area.getWidth()-length, this.area.getHeight()-thickness,length, thickness);
        
        //-----------------------------
        length = this.RACKETLENGTH * (this.area.getHeight()/100); //Größe der Rackets 
        thickness = this.area.getWidth()/50; //Dicke der Rackets (0,3 % der Spielfeldbreite)
        
        this.rackets = new Racket[3];
        this.rackets[0] = new Racket(0,(this.area.getHeight()/2)-(length/2),thickness, length);
        this.rackets[1] = new Racket(this.area.getWidth()-thickness,(this.area.getHeight()/2)-(length/2),thickness, length);
        this.rackets[2] = new Racket((this.area.getWidth()/2)-(length/2), this.area.getHeight()-thickness, length, thickness);
        
        //-----------------------------
        this.players = new Playerlifes[3];
        this.players[0] = new Playerlifes(this.area.getWidth()*0.45f, this.area.getHeight()*0.1f);
        this.players[1] = new Playerlifes(this.area.getWidth()*0.55f, this.area.getHeight()*0.1f);
        this.players[2] = new Playerlifes(this.area.getWidth()*0.50f, this.area.getHeight()*0.1f);
        
        
        //-----------------------------
        this.resetPong(); // Einstellung des Pongs werden gesetzt
        
        //-----------------------------
        Drawable[] draw = new Drawable[11];
        draw[0] = this;
        draw[1] = this.pong;
        draw[2] = this.rackets[0];
        draw[3] = this.rackets[1];
        draw[4] = this.rackets[2];
        draw[5] = this.borders[0];
        draw[6] = this.borders[1];
        draw[7] = this.borders[2];
        draw[8] = this.players[0];
        draw[9] = this.players[1];
        draw[10] = this.players[2];
        
        
        
        return draw;
        
    }
    
    /**
     * Startet 2-Spieler-Spiel.
     * Initialisiert alle Objekte und setzt Pong zurück.
     * @return Zeiger auf alle zu rendernden Objekte.
     */
    public Drawable[] initTwoPlayerGame(){
        this.amountPlayers = 2;
        
        this.borders = new Border[3];
        this.borders[0] = new Border(0,0,this.area.getWidth(),this.area.getHeight()/20);
        this.borders[1] = new Border(0,this.area.getHeight()-(this.area.getHeight()/20),this.area.getWidth(),this.area.getHeight()/20);
        this.borders[2] = new Border(this.area.getWidth()/4, this.area.getHeight()/2, 2*(this.area.getWidth()/4), this.area.getHeight()/50);
        
        //-----------------------------
        int length = this.RACKETLENGTH * (this.area.getHeight()/100); //Größe der Rackets 
        int thickness = this.area.getWidth()/50; 
        
        //-----------------------------
        this.rackets = new Racket[2];
        this.rackets[0] = new Racket(0,(this.area.getHeight()/2)-(length/2),thickness, length);
        this.rackets[1] = new Racket(this.area.getWidth()-thickness,(this.area.getHeight()/2)-(length/2), thickness, length);
       
        //-----------------------------
        this.players = new Playerlifes[2];
        this.players[0] = new Playerlifes(this.area.getWidth()*0.45f, this.area.getHeight()*0.1f);
        this.players[1] = new Playerlifes(this.area.getWidth()*0.55f, this.area.getHeight()*0.1f);
        
        //-----------------------------
        this.resetPong(); // Einstellung des Pongs werden gesetzt
        
        //-----------------------------
        Drawable[] draw = new Drawable[9];
        draw[0] = this;
        draw[1] = this.pong;
        draw[2] = this.rackets[0];
        draw[3] = this.rackets[1];
        draw[4] = this.borders[0];
        draw[5] = this.borders[1];
        draw[6] = this.borders[2];
        draw[7] = this.players[0];
        draw[8] = this.players[1];
        
        return draw;
        
    }
    
    /**
     * Bewegt den Ball des Spiels. Prueft erst auf Kollisionen mit Objekten und
     * setzt den Ball schlussendlich einen Schritt weiter in die determinierte Richtung.
     * 
     *      Folgende Faelle werden unterschieden:
     *      Keine Kollision: Ball bewegt sich geradlinig
     *      Kollision mit Border: Ball wird spiegelnd reflektiert
     *      Kollision mit Racket: Ball wird reflektiert. Der Ausfallswinkel ist abhaengig vom Einfallswinkel und
     *                            dem Auftreffpunkt auf dem Racket. Trifft man das Racket am unteren Ende wird der
     *                            Ball nach unten abgelenkt. Am oberen Ende nach oben. Ein Auftreffen in der Mitte
     *                            resultiert in einer spiegelnden Reflektion.
     * 
     * @return Gibt zurück, ob eine Kollision mit einem Racket stattgefunden hat.
     */
    public boolean movePong(){
        // Bewegungsgeschwindigkeiten auf ganz Zahlen runden, damit der Ball nicht bei Werten unter 1.0 stehen bleibt
        int speedXRounded = this.round(this.pong.getSpeedX());
        int speedYRounded = this.round(this.pong.getSpeedY());
        // Pong Area berechnen wie sie nach dem nächsten Bewegungsschritt wäre
        Rectangle newPongArea = new Rectangle((int) (this.pong.getArea().getTopx()+speedXRounded),
                                           (int) (this.pong.getArea().getTopy()+speedYRounded),
                                            this.pong.getArea().getWidth(),
                                            this.pong.getArea().getHeight());
        
        // Test 1:
        //Racket Collission ??--------------------------------------------------
        for(int i= 0; i < this.rackets.length; i++){
            if(newPongArea.collidesWith(this.rackets[i].getArea())){
                
                Rectangle racketArea = rackets[i].getArea();
                char impactside = this.getImpactSide(newPongArea, racketArea);
                float impactDerivation;
                
                //Ball Geschindigkeit manipulieren je nach Einschlagsseite
                if(impactside == 't' || impactside == 'b' ){
                    impactDerivation = this.getImpactDerivation(racketArea.getTopx(),
                                                                      racketArea.getWidth(),
                                                                      newPongArea.getTopx(),
                                                                      newPongArea.getWidth());
                    this.pong.setSpeedY(-this.pong.getSpeedY());
                    float oldSpeedX = this.pong.getSpeedX();
                    float oldSpeedY = this.pong.getSpeedY();
                    float oldTotalSpeed = this.pong.getTotalSpeed();
                    
                    if (impactside == 'b') {
                        impactDerivation = -impactDerivation;
                    }
                    
                    //Neuen Austrittswinkel bestimmen
                    this.pong.changeMovingAngle(impactDerivation);
                    
                    // BUGFIX: Bei zu spitzen Einfallswinkeln wird Pong unter best. Umstaenden in Border rein reflektiert
                    //         In diesem Fall wird eine spiegelnde Kollision angewendet
                    if (  (oldSpeedY>0 && this.pong.getSpeedY()<0) || (oldSpeedY<0 && this.pong.getSpeedY()>0)  ) {
                        System.out.println("---Speed zurueckgesetzt---");
                        this.pong.setSpeedX( oldSpeedX );
                        this.pong.setSpeedY( oldSpeedY );
                    }
                    // BUGFIX: Pong wird manchmal nicht mit der gleichen Geschwindigkeit reflektiert
                    //         Dieser Bugfix beschleunigt/verlangsamt den Pong wieder auf die urspruengliche Geschw.
                    else {
                        float newTotalSpeed = this.pong.getTotalSpeed();
                        this.pong.setSpeedX( this.pong.getSpeedX() * (oldTotalSpeed/newTotalSpeed) );
                        this.pong.setSpeedY( this.pong.getSpeedY() * (oldTotalSpeed/newTotalSpeed) );
                    }
                }
                
                else if(impactside == 'l' || impactside == 'r'){
                    impactDerivation = this.getImpactDerivation(racketArea.getTopy(),
                                                                      racketArea.getHeight(),
                                                                      newPongArea.getTopy(),
                                                                      newPongArea.getHeight());
                    this.pong.setSpeedX(-this.pong.getSpeedX());
                    
                    //Benoetigte Variablen fuer Bufixes initialisieren
                    float oldSpeedX = this.pong.getSpeedX();
                    float oldSpeedY = this.pong.getSpeedY();
                    float oldTotalSpeed = this.pong.getTotalSpeed();
                   
                    if (impactside == 'l') {
                        impactDerivation = -impactDerivation;
                    }
                    
                    //Neuen Austrittswinkel bestimmen
                    this.pong.changeMovingAngle(impactDerivation);
                    
                    // BUGFIX: Bei zu spitzen Einfallswinkeln wird Pong unter best. Umstaenden in Border rein reflektiert
                    //         In diesem Fall wird eine spiegelnde Kollision angewendet
                    if (  (oldSpeedX>0 && this.pong.getSpeedX()<0) || (oldSpeedX<0 && this.pong.getSpeedX()>0)  ) {
                        System.out.println("---Speed zurueckgesetzt---");
                        this.pong.setSpeedX( oldSpeedX );
                        this.pong.setSpeedY( oldSpeedY );
                    }
                    // BUGFIX: Pong wird manchmal nicht mit der gleichen Geschwindigkeit reflektiert
                    //         Dieser Bugfix beschleunigt/verlangsamt den Pong wieder auf die urspruengliche Geschw.
                    else {
                        float newTotalSpeed = this.pong.getTotalSpeed();
                        this.pong.setSpeedX( this.pong.getSpeedX() * (oldTotalSpeed/newTotalSpeed) );
                        this.pong.setSpeedY( this.pong.getSpeedY() * (oldTotalSpeed/newTotalSpeed) );
                    }
                }
                
                //Gibt Angaben zur Geschwindigkeit aus, falls gewünscht.
                if (this.BUGFIX) {
                    System.out.println("V_neu:\t"+this.pong.getTotalSpeed()+"\tV_x\t"+this.pong.getSpeedX()+"\tV_y\t"+this.pong.getSpeedY());
                }
                                
                //Pong nach Racket-Kollission beschleunigen
                this.pong.accellerate(this.ACCELLERATION);
                
                
                //Pong um einen Bewegungschritt bewegen. Der uebergebene Wert bedeutet,
                //dass Ball normal bewegt wird, ohne Geschwindigkeiten aufzurunden.
                this.pong.moveAStep(false);
                
                //Rueckgabe: Es hat eine Kollission mit einem Racket stattgefunden
                return true;              
            }            
        }
        
        // Test 2:
        //Kollission mit einer Border? ----------------------------------------
        for(int i = 0; i < this.borders.length; i++){
            if(newPongArea.collidesWith(borders[i].getArea())){
                
                char impactside = this.getImpactSide(newPongArea, borders[i].getArea());
                
                //Ball Geschindigkeit manipulieren je nach Einschlagsseite
                if(impactside == 't' || impactside == 'b' ){
                    this.pong.setSpeedY(this.pong.getSpeedY()*(-1.0f));
                }
                if(impactside == 'l' || impactside == 'r'){
                    this.pong.setSpeedX(this.pong.getSpeedX()*(-1.0f));
                }
                                
                //Pong nach Kollission bewegen. Parameter true lässt SpeedX und SpeedY auf min. 1 aufrunden, damit Ball nach Kollision nicht an Border stuckt.
                this.pong.moveAStep(true);
                
                //Rückgabe: Es hat eine Kollission mit einem Racket stattgefunden
                return true;                      
            }
        }
        //Pong nach keiner Kollission bewegen
        this.pong.setArea(newPongArea);
        return false;
    }
    
    /**
     * Rundet eine Zahl im float Format in int Format. Rundet bei weniger als 0.5 ab, bei mehr auf. Beruecksichtigt auch negative Nummern.
     * @param number
     * @return Gerundete int-Variable
     */
    private int round (float number) {
        if (number >= 0) {
            return (int) (number+0.5f);
        } else {
            return (int) (number-0.5f);
        }
    }
    
    /**
     * Berechnet den Ablenkungswinkel bei der Kollision des Pongs mit einem Racket.
     * Das Racket ist als staticLine definiert. Der Pong als MovingLine.
     * Je nach Kollisionsseite werden die Daten der x-Position und x-Laenge genommen oder die Daten y-Seite
     * 
     * Erklärung Return Value
     *   Nimmt Werte von:
     * -2 * derivationFactor wenn MovingLine startPosStaticLine mit einem Pixel berührt 
     * +2 * derivationFactor wenn MovingLine das Ende von StaticLine mit einem Pixel berührt
     * 
     * an.
     *
     * @param startPosStaticLine 
     * @param lengthStaticLine
     * @param startPosMovingLine
     * @param lengthMovingLine
     * @return sh. oben
     */
    private float getImpactDerivation(int startPosStaticLine, int lengthStaticLine, int startPosMovingLine, int lengthMovingLine){
        float impactDerivation;
        
        float currentImpactPos = (float) (startPosMovingLine + lengthMovingLine - startPosStaticLine);
        float maximumImpactPos = (float) (lengthStaticLine + lengthMovingLine);
        
        /* Erklärung Impact Derivation
         *   Nimmt Werte von 0...1 an:
         * 0 wenn MovingLine startPosStaticLine mit einem Pixel berührt 
         * 1 wenn MovingLine das Ende von StaticLine mit einem Pixel berührt
         */
        impactDerivation = (currentImpactPos/maximumImpactPos); 
        
        // impactDerivation nimmt einen Wert zwischen -(DERIVATION/2) und +(DERIVATION/2) an
        impactDerivation = (impactDerivation * this.DERIVATION ) - (this.DERIVATION / 2);
        
        
        return impactDerivation;
    }
//####################################################################################################################
   
    /**
     * Berechnet bei einer Kollission von zwei Rechtecken, die sich schon Flächenmäßig übereinander geschoben haben
     * die Seite des statischen Rechtecks auf der das sich bewegende Rechteck eingeschlagen ist
     * 
     * @param movingR Bewegendes Rechteck, das auf das statische Rechteck einschlägt
     * @param staticR Statisches Rechteck
     * @return Die Seite des statischen Rechtecks auf der das sich bewegende Rechteck einschlägt
     */
    private char getImpactSide(Rectangle movingR, Rectangle staticR){

                //Eintauchtiefe der Rechtecke berechnen. Theorie: die Seite mit der kleinsten Eintauchtiefe ist die Seite
                //auf der die Kollission stattgefunden hat
                
                int[] depth = new int[4];
                depth[0] = movingR.getTopy() + movingR.getHeight() - staticR.getTopy(); //Top
                depth[1] = staticR.getTopx()+ staticR.getWidth() - movingR.getTopx(); //Right
                depth[2] = staticR.getTopy()+ staticR.getHeight()- movingR.getTopy(); //Bottom
                depth[3] = movingR.getTopx() + movingR.getWidth() - staticR.getTopx(); //Left
                
                char[] sides = new char[4];
                sides[0] = 't';
                sides[1] = 'r';
                sides[2] = 'b';
                sides[3] = 'l';
                
                //Kleinste Eintauchtiefe der Rechtecke herausfinden mit Hilfe von Bubble Sort
                int tempD;
                char tempS;
                boolean swapped;                
                do{
                    swapped = false;
                    for(int j = 1; j<depth.length;j++){
                        if(depth[j-1] > depth[j]){
                            //Drehen der Eintauchtiefe
                            tempD = depth[j-1];
                            depth[j-1] = depth[j];
                            depth[j] = tempD;
                            //Drehen der Seitenbezeichnung
                            tempS = sides[j-1];
                            sides[j-1] = sides[j];
                            sides[j] = tempS;
                            
                            swapped = true;
                        }
                    }                    
                }while(swapped);
                return sides[0];
    }
    
    public void exit(){
        //Not implemented yet
        //Soll beim Ende eines Spiels ausgeführt werden
    }
    
    /**
     * Zeigt bei Bedarf die Verlustmeldung eines Spielers an.
     * @param container
     * @param g 
     */
    @Override
    public void draw(GameContainer container, Graphics g) {
        for (int i=0; i<this.players.length; i++) {
            if (this.players[i].getLifes() == 0)
                g.drawString("Spieler "+(i+1)+" hat verloren!!", Arena.ARENAWIDTH*0.37f*GuiPlay.XFACTOR, Arena.ARENAHEIGHT*0.45f*GuiPlay.YFACTOR);
        }
    }
    
   
    /**
     * Bewegt die Schläger der Spieler
     * 
     * Bedeutung 'u' für Player bottom und top: bewege nach rechts
     * Bedeutung 'd' für Player bottom und top: bewege nach links
     * @param player
     * @param command 
     */
    //Racket Bewegungsmethoden-------------------------
    public void moveRacket(CInput input){
        float speed = (this.RACKETSPEED * (float)this.area.getHeight()) + (this.RACKETSPEED * (float)this.area.getWidth()); //Racketspeedangepasst an die arena
        
        switch(input.getCommand()){
            
            case CInput.UP: 
                switch(input.getPlayer()){
                    case CInput.LEFT:   rackets[0].setSpeedy(-speed); moveSingleRacket(rackets[0]); break;
                    case CInput.RIGHT:   rackets[1].setSpeedy(-speed); moveSingleRacket(rackets[1]); break;
                    case CInput.BOTTOM:   rackets[2].setSpeedx(speed); moveSingleRacket(rackets[2]); break;
                    case CInput.TOP:   rackets[3].setSpeedx(speed); moveSingleRacket(rackets[3]); break;
                }break;
            
            case CInput.DOWN: 
                switch(input.getPlayer()){
                    case CInput.LEFT:   rackets[0].setSpeedy(speed); moveSingleRacket(rackets[0]);break;
                    case CInput.RIGHT:   rackets[1].setSpeedy(speed); moveSingleRacket(rackets[1]);break;
                    case CInput.BOTTOM:   rackets[2].setSpeedx(-speed); moveSingleRacket(rackets[2]);break;
                    case CInput.TOP:   rackets[3].setSpeedx(-speed); moveSingleRacket(rackets[3]);break;
                }break;
            
                
            default: break;
                
        }
    }

 /**
 * Bewegt ein Racket um racket.speedx, racket.speedy und prüft vorher auf Kollisionen
 * 
 * @param racket Zeiger auf zu bewegendes racket
 */
private void moveSingleRacket(Racket racket){
    boolean collides = false;
    
    Rectangle oldRacketArea = racket.getArea();
    Rectangle newRacketArea = new Rectangle(oldRacketArea.getTopx() + (int)racket.getSpeedx(),oldRacketArea.getTopy() + (int)racket.getSpeedy(),oldRacketArea.getWidth(),oldRacketArea.getHeight());
    
    //Racket kollidiert mit einer Border ?
    for(int i = 0; i<borders.length;i++){
        if(collides == false){
            collides = newRacketArea.collidesWith(borders[i].getArea());
        }
    }
    //Racket kollidiert mit Pong ?
    if(collides == false){
        collides = newRacketArea.collidesWith(this.pong.getArea());
    }
    if(!collides){
        racket.moveAStep();           
    }
}

/**
 * Testet, ob Pong außerhalb des Spiels ist
 * @return false, falls Pong im Spielfeld
 */
public boolean isPongOut() {
        return !this.area.collidesWith(this.pong.getArea());
    }

/**
 * Testet, ob der Pong außerhalb der Arena ist.
 * 
 * Return: 
 * r : rechts außen
 * l : links
 * b : unten (bottom)
 * t : oben (top)
 * e : Error
 * 
 * @return 
 */
    public char pongOutAtSide() {
        Rectangle ball = this.pong.getArea();
        
        int x = ball.getTopx() + ball.getWidth()/2;
        int y = ball.getTopy() + ball.getHeight()/2;
        
        if(x > this.area.getWidth()){ //Ball ist rechts von der Arena
            return 'r';
        }else if(x < this.area.getTopx()){ //Ball ist links von der Arena
            return 'l';
        }else if(y > this.area.getHeight()){ //Ball ist unter der Arena
            return 'b';
        }else if(y < this.area.getTopy()){ //Ball ist über der Arena
            return 't';
        }else{ 
            return 'e'; //Ansonten e error
        }
   }
    
/**
 * Setzt den Pong auf die Anfangsposition mit Anfangsgeschwindigkeit zurück.<br />
 * Die Anfangsgeschwindigkeit erhöht sich bei jedem Reset um die Konstante INITACCELLERATION.
 */
    public void resetPong() {
        int diameterPong = this.PONGDIAMETER * (this.area.getWidth()/1000); //Pong Durchmesser setzen
        
        float speedx, speedy;
        
        do {
            double startingAngle = Math.random()*Math.PI*2; //Abschusswinkel des Pongs
        
            speedx = (float) (Arena.ARENAWIDTH * 0.001 * startSpeedPong * Math.sin(startingAngle));
            speedy = (float) (Arena.ARENAWIDTH * 0.001 * startSpeedPong * Math.cos(startingAngle));
        }
        //BUGFIX:   Verhindert, dass der Ball zu vertikal startet.
        while (speedx < (Arena.ARENAWIDTH * 0.0005) && speedx > (Arena.ARENAWIDTH * -0.0005));
        
        //Pong wird gesetzt mit Durchmesser, Position, Geschwindigkeit x und y
        this.pong.init(diameterPong,this.area.getWidth()/2-(diameterPong/2),this.area.getHeight()/3-(diameterPong/2),speedx, speedy);
        
        //Anfangsgeschwingkeit erhöhen
        this.startSpeedPong = this.startSpeedPong * this.INITACCELLERATION;
        
    }
    
    /**
     * Gibt ein Array auf alle Spielpunktezähler zurück.
     * 
     * @return Array aus Playerpoints Objekten
     */
    public Playerlifes[] getPlayers () {
        return this.players;
    }
}
